Fix use-after-free during lazy object initialization - #22401
Conversation
morrisonlevi
left a comment
There was a problem hiding this comment.
I can't really comment about the fix but here's one small thing I did notice.
| zval garbage; | ||
| ZVAL_COPY_VALUE(&garbage, p); | ||
| ZVAL_UNDEF(p); | ||
| i_zval_ptr_dtor(&garbage); |
There was a problem hiding this comment.
zend_lazy_object_revert_init is entirely off the hot path, right? I think zval_ptr_dtor would be more appropriate to avoid excess code size?
There was a problem hiding this comment.
Right, switched both new dtors to zval_ptr_dtor.
zend_lazy_object_revert_init() and zend_lazy_object_init_proxy() dropped a property value's refcount while the slot or dynamic-properties table still aliased it, so a value whose destructor reaches back through a reference cycle (unset($this->obj->prop)) freed it twice. Clear the slot and detach the properties table before dropping the refcount. Fixes phpGH-22399
d924213 to
7a36177
Compare
|
@iliaal With Claude's help I found that this same issue happens for ghosts and proxies in another place. Pushed two test cases and a fix. The comments are a bit verbose, I might cut them down, but they explain how the VM state is being altered which is critical for me, I can't follow these bugs without these kinds of comments! |
Don't mind them personally, I find them more helpful and being inside a test hardly harmful to code flow |
|
The extra fix makes sense to me, thanks +1 from me on that bit |
| && ((obj->ce->ce_flags & ZEND_ACC_FINAL) || (prop_info->flags & ZEND_ACC_FINAL))) { | ||
| continue; | ||
| } | ||
| zend_object_dtor_property(obj, p); |
There was a problem hiding this comment.
FWIU zend_object_dtor_property() was extracted to a function for lazy objects. With these cases outlined, only the original caller remains. It might make more sense to outline that one as an optimization, and switch this to zval_ptr_dtor() in zend_object_dtor_property().
There was a problem hiding this comment.
Confirmed, it landed in the Lazy objects commit (58aa6fc) and zend_object_std_dtor() is now its only caller.
The lazy paths can't reuse it as-is though: they have to null the slot before dropping the refcount (that's the actual fix), and they already hold prop_info, while zend_object_dtor_property() re-derives it from the slot pointer. Sharing it would mean changing its signature.
Did you mean inlining it into zend_object_std_dtor() as single-caller cleanup, or reworking it so the lazy paths can share? Either way I'd keep that on master and leave this 8.4 PR as the minimal UAF fix, unless you'd prefer it here.
Lazy object initialization destroyed property values while the slot or the dynamic-properties
HashTablestill aliased them, so a value whose__destructreaches back through a reference cycle (unset($this->obj->prop)) double-freed it, a UAF inzend_objects_store_del. The fix clears the slot and detaches the properties table before dropping the refcount, across all four destruction sites inzend_lazy_object_revert_initandzend_lazy_object_init_proxy(declared and dynamic properties, ghost revert and proxy cleanup); verified under ASAN.Fixes GH-22399